﻿using System;
using System.Diagnostics.Contracts;
using System.Threading;

namespace O1.IO
{
    [ContractVerification(true)]
    internal sealed class DmlCommandBuffer : IDisposable
    {
        private const int BitIndexLog = 3;

        private readonly byte[] control;

        private readonly byte[] buffer;

        private readonly long transactionId;

        private short count;

        private int bufferOffset;

        private bool disposed;

        public DmlCommandBuffer(long transactionId)
        {
            this.control = BufferCache1K.Acquire();
            this.buffer = BufferCache8K.Acquire();
            this.transactionId = transactionId;
        }

        public void TrySendAdd(uint cellId, byte[] data, int startOffset)
        {
            Contract.Requires(data != null);
            Contract.Requires(data.Length <= BufferCache8K.Size - 12);

            Contract.Ensures(this.count >> BitIndexLog <= BufferCache1K.Size);

            this.CheckDisposed();

            if (this.bufferOffset + data.Length + 12 > BufferCache8K.Size)
            {
                this.Flush();
            }

            Contract.Assume(this.count >> BitIndexLog < BufferCache1K.Size);
            this.control[this.count >> BitIndexLog] |= (byte)(this.count & 0xF);
            this.count++;

            cellId.CopyBytes(this.buffer, this.bufferOffset);
            this.bufferOffset += Sz.CLR.Int32;
            startOffset.CopyBytes(this.buffer, this.bufferOffset);
            this.bufferOffset += Sz.CLR.Int32;
            data.Length.CopyBytes(this.buffer, this.bufferOffset);
            this.bufferOffset += Sz.CLR.Int32;
            Array.Copy(data, 0, this.buffer, this.bufferOffset, data.Length);

            if (this.count >> BitIndexLog >= BufferCache1K.Size)
            {
                this.Flush();
            }

            Contract.Assert(this.count >> BitIndexLog < BufferCache1K.Size);
        }

        public void TrySendDelete(uint cellId, int startOffset, int size)
        {
            Contract.Requires(startOffset >= 0);
            Contract.Requires(size >= 0);

            Contract.Ensures(this.count >> BitIndexLog <= BufferCache1K.Size);

            this.CheckDisposed();
            if (this.bufferOffset + (Sz.CLR.Int32 * 3) > BufferCache8K.Size)
            {
                this.Flush();
            }

            Contract.Assume(this.count >> BitIndexLog < BufferCache1K.Size);
            this.count++;
            
            cellId.CopyBytes(this.buffer, this.bufferOffset);
            this.bufferOffset += Sz.CLR.Int32;
            startOffset.CopyBytes(this.buffer, this.bufferOffset);
            this.bufferOffset += Sz.CLR.Int32;
            size.CopyBytes(this.buffer, this.bufferOffset);
            this.bufferOffset += Sz.CLR.Int32;

            if (this.count >> BitIndexLog >= BufferCache1K.Size || this.bufferOffset + 12 > BufferCache8K.Size)
            {
                this.Flush();
            }

            Contract.Assert(this.count >> BitIndexLog < BufferCache1K.Size);
        }

        public void Dispose()
        {
            if (this.disposed)
            {
                return;
            }

            if (this.count > 0)
            {
                this.Flush();
            }

            this.disposed = true;
            GC.SuppressFinalize(this);
        }

        private void Flush()
        {
            Contract.Ensures(this.count == 0);
            Contract.Ensures(this.bufferOffset == 0);
            Contract.Ensures(this.count >> BitIndexLog < BufferCache1K.Size);

            var handle = DmlCommandQueue.EnqueueCommand(this.control, this.buffer, this.transactionId, this.count);
            if (!handle.Complete)
            {
                var spin = new SpinWait();
                while (!handle.Complete)
                {
                    spin.SpinOnce();
                }
            }

            this.count = 0;
            this.bufferOffset = 0;
        }

        private void CheckDisposed()
        {
            if (this.disposed)
            {
                throw new ObjectDisposedException(typeof(DmlCommandBuffer).Name);
            }
        }

        [ContractInvariantMethod]
        private void ObjectInvariant()
        {
            Contract.Invariant(this.control != null);
            Contract.Invariant(this.control.Length == BufferCache1K.Size);
            Contract.Invariant(this.buffer != null);
            Contract.Invariant(this.buffer.Length == BufferCache8K.Size);
            
            Contract.Invariant(this.bufferOffset >= 0);
            Contract.Invariant(this.count >= 0);
            Contract.Invariant(this.count >> BitIndexLog <= BufferCache1K.Size);
        }
    }
}
